from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.dml.color import MSO_COLOR_TYPE, RGBColor as PPTX_RGBColor
from pptx.dml.fill import MSO_FILL
import os
import zipfile
import xml.etree.ElementTree as ET
import re

LOG_FILE = os.path.join('output', 'run.log')
def log_print(*args, **kwargs):
    msg = ' '.join(str(a) for a in args)
    print(msg, **kwargs)
    try:
        with open(LOG_FILE, 'a', encoding='utf-8') as f:
            f.write(msg + '\n')
    except Exception as e:
        print(f'[日志写入失败] {e}')
log_print('parser.py 已加载')

# 在文件顶部添加全局变量
missed_theme_keys = []

# 主题色key标准化映射
THEME_KEY_ALIAS = {
    'BG1': 'LT1',
    'BG2': 'DK1',
    'TX1': 'DK1',
    'TX2': 'LT1',
    'BACKGROUND1': 'LT1',
    'BACKGROUND2': 'DK1',
    'TEXT1': 'DK1',
    'TEXT2': 'LT1',
}
def normalize_theme_key(key):
    k = key.upper().replace('_', '')
    return THEME_KEY_ALIAS.get(k, k)

def hex_to_rgb(hex_str):
    hex_str = hex_str.strip().lstrip('#')
    if len(hex_str) == 6:
        return tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4))
    raise ValueError(f'Invalid hex color: {hex_str}')

def rgb_to_hex(rgb):
    return '{:02X}{:02X}{:02X}'.format(*rgb)

def apply_lum_mod_off(rgb, lum_mod=None, lum_off=None):
    # lum_mod/lum_off为百分比（如90000=90%），均为整数
    r, g, b = rgb
    if lum_mod is not None:
        r = int(r * lum_mod / 100000)
        g = int(g * lum_mod / 100000)
        b = int(b * lum_mod / 100000)
    if lum_off is not None:
        r = min(255, int(r + 255 * lum_off / 100000))
        g = min(255, int(g + 255 * lum_off / 100000))
        b = min(255, int(b + 255 * lum_off / 100000))
    return (r, g, b)

# 新增：统一主题色查表函数

def resolve_theme_color(key, theme_color_map, clrmap=None):
    norm_key = key.upper().replace('_', '')
    if clrmap:
        log_print(f'[clrMap调试] clrMap内容: {clrmap}')
    if clrmap and norm_key.lower() in clrmap:
        mapped_key = clrmap[norm_key.lower()]
        mapped_key = mapped_key.upper().replace('_', '')
        log_print(f'[clrMap调试] key={key} 命中clrMap: {norm_key.lower()}->{mapped_key}')
    else:
        mapped_key = norm_key
        log_print(f'[clrMap调试] key={key} 未命中clrMap, 用原key: {mapped_key}')
    result = theme_color_map.get(mapped_key)
    log_print(f'[clrMap调试] theme_color_map查表: mapped_key={mapped_key}, result={result}')
    return result

# 修改 get_color_info

def get_color_info(fill_or_line, theme_color_map=None, clrmap=None):
    color = None
    color_type = None
    try:
        fc = getattr(fill_or_line, 'fore_color', None)
        color_type = getattr(fc, 'type', None)
        log_print(f'[调试] get_color_info: fore_color={fc}, color_type={color_type}, theme_color={getattr(fc, "theme_color", None)}, rgb={getattr(fc, "rgb", None)}')
        if color_type == MSO_COLOR_TYPE.RGB:
            color = str(fc.rgb)
        elif color_type == MSO_COLOR_TYPE.SCHEME:
            theme_color = getattr(fc, 'theme_color', None)
            if theme_color and theme_color_map:
                if hasattr(theme_color, 'name'):
                    raw_key = theme_color.name
                else:
                    raw_key = str(theme_color)
                if '(' in raw_key:
                    key = raw_key.split('(')[0].strip()
                else:
                    key = raw_key
                key = normalize_theme_key(key)
                log_print(f'[调试] theme_color原始: {theme_color}, raw_key: {raw_key}')
                log_print(f'[调试] theme_color最终key: {key}')
                base_rgb = resolve_theme_color(key, theme_color_map, clrmap)
                elem = getattr(fill_or_line, 'parent', None)
                if elem is not None:
                    ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
                    scheme_clr = elem.find('.//a:schemeClr', ns)
                    lum_mod = None
                    lum_off = None
                    if scheme_clr is not None:
                        for child in scheme_clr:
                            if child.tag.endswith('lumMod'):
                                lum_mod = int(child.attrib.get('val', '100000'))
                            if child.tag.endswith('lumOff'):
                                lum_off = int(child.attrib.get('val', '0'))
                    if base_rgb:
                        rgb = hex_to_rgb(base_rgb)
                        rgb = apply_lum_mod_off(rgb, lum_mod, lum_off)
                        color = rgb_to_hex(rgb)
                    else:
                        color = None
                else:
                    color = base_rgb or None
                log_print(f'[调试] 主题色查表: key={key}, color={color}, theme_color_map={theme_color_map}')
            else:
                color = None
        else:
            color = None
        if hasattr(fill_or_line, 'background') and fill_or_line.background:
            bg_fill = fill_or_line.background
            if bg_fill.type == MSO_FILL.SOLID:
                return get_color_info(bg_fill.fore_color, theme_color_map, clrmap)
    except Exception as e:
        log_print(f'[调试] get_color_info异常: {e}')
        color = None
    log_print(f'[调试] get_color_info结果: color={color}, color_type={color_type}')
    return color, 'RGB' if color else None


def get_shadow_info(shadow):
    if not shadow:
        return None
    
    # 调试阴影对象属性
    log_print(f'[调试] 阴影属性: visible={getattr(shadow, "visible", None)}, inherit={getattr(shadow, "inherit", None)}, type={type(shadow)}')
    
    # 检查阴影是否真的存在 - 即使visible为None但有其他参数时，也认为有阴影
    shadow_exists = False
    try:
        if hasattr(shadow, 'visible') and shadow.visible:
            shadow_exists = True
        elif hasattr(shadow, 'blur_radius') and shadow.blur_radius > 0:
            shadow_exists = True
        elif hasattr(shadow, 'distance') and shadow.distance > 0:
            shadow_exists = True
    except Exception as e:
        log_print(f'[调试] 阴影检测异常: {e}')
        
    info = {
        'visible': shadow_exists, # 根据综合判断设置visible
        'blur_radius': float(getattr(shadow, 'blur_radius', 0)),
        'distance': float(getattr(shadow, 'distance', 0)),
        'direction': float(getattr(shadow, 'direction', 0)),
        'transparency': float(getattr(shadow, 'transparency', 0)),
        'color': None
    }
    try:
        color = getattr(shadow, 'color', None)
        if color and getattr(color, 'type', None) == MSO_COLOR_TYPE.RGB:
            info['color'] = str(color.rgb)
        elif color and getattr(color, 'type', None) == MSO_COLOR_TYPE.SCHEME:
            theme_color = getattr(color, 'theme_color', None)
            if theme_color:
                theme_str = str(theme_color)
                info['color'] = f"SCHEME({theme_str})"
    except Exception as e:
        log_print(f'[调试] 阴影颜色解析异常: {e}')
    
    # 如果有任何非零参数，认为阴影存在
    if info['blur_radius'] > 0 or info['distance'] > 0 or info['color']:
        info['visible'] = True
        
    log_print(f'[调试] 阴影信息结果: {info}')
    return info


def get_shape_styles(shape, theme_color_map=None, clrmap=None):
    fill = getattr(shape, 'fill', None)
    line = getattr(shape, 'line', None)
    shape_name = getattr(shape, "name", "")
    shape_type = getattr(shape, 'shape_type', None)
    # 仅对明确支持shadow的类型访问shadow属性，其它类型一律跳过
    shadow_supported_types = [MSO_SHAPE_TYPE.PICTURE, MSO_SHAPE_TYPE.AUTO_SHAPE, MSO_SHAPE_TYPE.TEXT_BOX]
    if shape_type in shadow_supported_types:
        log_print(f'[调试] get_shape_styles: name={shape_name}, fill={fill}, line={line}, shadow={getattr(shape, "shadow", None)})')
    else:
        log_print(f'[调试] get_shape_styles: name={shape_name}, fill={fill}, line={line}, shadow=SKIPPED_TYPE')

    styles = {}
    fill_color_found = False
    if fill and hasattr(fill, 'type'):
        if getattr(fill, 'type', None) is None or (hasattr(fill, 'transparency') and fill.transparency == 1.0):
            styles['fill'] = None
            fill_color_found = True
        elif fill.type == MSO_FILL.SOLID:
            try:
                fc = getattr(fill, 'fore_color', None)
                color_type = getattr(fc, 'type', None)
                theme_color = getattr(fc, 'theme_color', None)
                rgb_value = getattr(fc, 'rgb', None)
                log_print(f'[调试] 填充色检查: fore_color={fc}, type={color_type}, theme_color={theme_color}, rgb={rgb_value}')
                if color_type == MSO_COLOR_TYPE.RGB and rgb_value:
                    styles['fill'] = (str(rgb_value), 'RGB')
                    fill_color_found = True
                elif color_type == MSO_COLOR_TYPE.SCHEME and theme_color and theme_color_map:
                    if hasattr(theme_color, 'name'):
                        raw_key = theme_color.name
                    else:
                        raw_key = str(theme_color)
                    if '(' in raw_key:
                        key = raw_key.split('(')[0].strip()
                    else:
                        key = raw_key
                    key = normalize_theme_key(key)
                    theme_color_value = resolve_theme_color(key, theme_color_map, clrmap)
                    log_print(f'[调试] 填充主题色查表: key={key}, color={theme_color_value}')
                    if theme_color_value:
                        styles['fill'] = (theme_color_value, 'RGB')
                        fill_color_found = True
                    else:
                        log_print(f'[调试] 填充主题色查表失败: key={key}')
                        styles['fill'] = (None, 'RGB')
                        fill_color_found = True
            except Exception as e:
                log_print(f'[调试] 填充色解析异常: {e}')
                styles['fill'] = (None, 'RGB')
                fill_color_found = True
    if 'fill' not in styles:
        styles['fill'] = None
    
    styles['fill_transparency'] = getattr(fill, 'transparency', None) if fill else None
    
    # 边框色
    if line and hasattr(line, 'color') and line.color is not None:
        color_type = getattr(line.color, 'type', None)
        log_print(f'[调试] get_shape_styles: line.color={line.color}, color_type={color_type}, theme_color={getattr(line.color, "theme_color", None)}, rgb={getattr(line.color, "rgb", None)}')
        color = None
        elem = getattr(shape, 'element', None)
        expect_key = None
        if color_type == MSO_COLOR_TYPE.SCHEME:
            theme_color = getattr(line.color, 'theme_color', None)
            if theme_color:
                if hasattr(theme_color, 'name'):
                    raw_key = theme_color.name
                else:
                    raw_key = str(theme_color)
                if '(' in raw_key:
                    expect_key = raw_key.split('(')[0].strip()
                else:
                    expect_key = raw_key
                expect_key = normalize_theme_key(expect_key)
        if elem is not None and color_type == MSO_COLOR_TYPE.SCHEME:
            color = extract_scheme_color_with_lum(elem, theme_color_map, expect_key, clrmap)
        elif color_type == MSO_COLOR_TYPE.RGB:
            color = str(line.color.rgb) if getattr(line.color, 'rgb', None) else None
        # 只要没有有效RGB或主题色，直接输出null
        if not color:
            styles['line'] = {'color': None, 'color_type': None}
        else:
            styles['line'] = {'color': color, 'color_type': 'RGB'}
        styles['line_width'] = getattr(line, 'width', None)
        styles['line_dash'] = getattr(line, 'dash_style', None)
    else:
        styles['line'] = {'color': None, 'color_type': None}
        styles['line_width'] = None
        styles['line_dash'] = None
    # 阴影详细参数
    shadow_info = None
    # 仅对明确支持shadow的类型访问shadow属性，其它类型一律跳过
    if hasattr(shape, 'shadow') and shape_type in shadow_supported_types:
        try:
            if shape.shadow:
                shadow_info = get_shadow_info(shape.shadow)
        except NotImplementedError:
            shadow_info = None

    # 新增：如未提取到有效阴影参数，尝试直接解析element中的<a:outerShdw>
    def is_empty_shadow(info):
        if not info:
            return True
        # 全部为0或None，或visible为False
        return not info.get('visible') and all(
            not info.get(k) for k in ['blur_radius', 'distance', 'direction', 'transparency', 'color']
        )

    if is_empty_shadow(shadow_info):
        try:
            elem = getattr(shape, 'element', None)
            if elem is not None:
                ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
                outershdw = elem.find('.//a:outerShdw', ns)
                if outershdw is not None:
                    # 解析属性
                    blur = float(outershdw.attrib.get('blurRad', 0))
                    dist = float(outershdw.attrib.get('dist', 0))
                    direction = float(outershdw.attrib.get('dir', 0))
                    algn = outershdw.attrib.get('algn')
                    rot_with_shape = outershdw.attrib.get('rotWithShape')
                    color = None
                    alpha = None
                    prst_clr = outershdw.find('.//a:prstClr', ns)
                    scheme_clr = outershdw.find('.//a:schemeClr', ns)
                    srgb_clr = outershdw.find('.//a:srgbClr', ns)
                    if prst_clr is not None and 'val' in prst_clr.attrib:
                        color = prst_clr.attrib['val']
                        alpha_elem = prst_clr.find('.//a:alpha', ns)
                        if alpha_elem is not None and 'val' in alpha_elem.attrib:
                            alpha = float(alpha_elem.attrib['val'])
                    elif scheme_clr is not None and 'val' in scheme_clr.attrib:
                        color = scheme_clr.attrib['val']
                        alpha_elem = scheme_clr.find('.//a:alpha', ns)
                        if alpha_elem is not None and 'val' in alpha_elem.attrib:
                            alpha = float(alpha_elem.attrib['val'])
                    elif srgb_clr is not None and 'val' in srgb_clr.attrib:
                        color = srgb_clr.attrib['val']
                        alpha_elem = srgb_clr.find('.//a:alpha', ns)
                        if alpha_elem is not None and 'val' in alpha_elem.attrib:
                            alpha = float(alpha_elem.attrib['val'])
                    shadow_info = {
                        'visible': True,
                        'blur_radius': blur,
                        'distance': dist,
                        'direction': direction,
                        'algn': algn,
                        'rot_with_shape': rot_with_shape,
                        'color': color,
                        'alpha': alpha,
                    }
                    log_print(f'[调试] 通过element解析到outerShdw: {shadow_info}')
        except Exception as e:
            log_print(f'[调试] element阴影解析异常: {e}')

    styles['shadow'] = shadow_info
    log_print(f'[调试] get_shape_styles结果: {styles}')

    # 判断是否为自定义形状（custom geometry）
    is_custom_geom = False
    try:
        elem = getattr(shape, 'element', None)
        if elem is not None:
            ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
            cust_geom = elem.find('.//a:custGeom', ns)
            if cust_geom is not None:
                is_custom_geom = True
    except Exception as e:
        log_print(f'[调试] 自定义形状判断异常: {e}')
    styles['is_custom_geom'] = is_custom_geom

    return styles


def safe_get_auto_shape_type(shape):
    try:
        return getattr(shape, 'auto_shape_type', None)
    except ValueError:
        return None


def get_clrmap_from_pptx(prs):
    pptx_path = getattr(prs, 'pptx_path', None)
    if not pptx_path or not os.path.isfile(pptx_path):
        return {}
    try:
        with zipfile.ZipFile(pptx_path, 'r') as zf:
            for name in zf.namelist():
                if name == 'ppt/slideMasters/slideMaster1.xml':
                    master_xml = zf.read(name)
                    root = ET.fromstring(master_xml)
                    ns = {'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'}
                    clrmap = root.find('.//p:clrMap', ns)
                    if clrmap is not None:
                        clrmap_dict = {k.lower(): v.lower() for k, v in clrmap.attrib.items()}
                        log_print(f'[clrMap调试] PPTX包内母版clrMap: {clrmap_dict}')
                        return clrmap_dict
    except Exception as e:
        log_print(f'[调试] PPTX包内解析母版clrMap异常: {e}')
    return {}


def get_theme_color_map(prs):
    color_map = {}
    pptx_path = getattr(prs, 'pptx_path', None)
    log_print(f'[调试] get_theme_color_map: pptx_path={pptx_path}, prs={prs}')
    if not pptx_path and hasattr(prs, 'part') and hasattr(prs.part, 'package'):
        pptx_path = getattr(prs.part.package, 'pkg_file', None)
    theme_xml = None
    if pptx_path and os.path.isfile(pptx_path):
        try:
            with zipfile.ZipFile(pptx_path, 'r') as zf:
                for name in zf.namelist():
                    if name.startswith('ppt/theme/theme') and name.endswith('.xml'):
                        log_print(f'[调试] 从PPTX内部读取: {name}')
                        theme_xml = zf.read(name)
                        break
        except Exception as e:
            log_print('[调试] 主题色底层XML解析异常:', e)
    if theme_xml:
        try:
            root = ET.fromstring(theme_xml)
            ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
            clr_scheme = root.find('.//a:clrScheme', ns)
            if clr_scheme is not None:
                for clr in clr_scheme:
                    clr_tag = clr.tag.split('}')[-1]
                    clr_tag_key = clr_tag.upper().replace('_', '')
                    srgb = clr.find('.//a:srgbClr', ns)
                    if srgb is not None and srgb.attrib.get('val'):
                        color_map[clr_tag_key] = srgb.attrib['val'].upper()
                        continue
                    sysclr = clr.find('.//a:sysClr', ns)
                    if sysclr is not None and sysclr.attrib.get('lastClr'):
                        color_map[clr_tag_key] = sysclr.attrib['lastClr'].upper()
                        continue
            if 'LT1' in color_map:
                color_map['BG1'] = color_map['LT1']
                color_map['TX2'] = color_map['LT1']
            if 'DK1' in color_map:
                color_map['BG2'] = color_map['DK1']
                color_map['TX1'] = color_map['DK1']
        except Exception as e:
            log_print('[调试] theme1.xml 解析异常:', e)
    if not color_map:
        color_map = {
            'DK1': '000000', 'LT1': 'FFFFFF',
            'DK2': '222222', 'LT2': 'EEEEEE',
            'ACCENT1': 'FF0000', 'ACCENT2': '00FF00', 'ACCENT3': '0000FF',
            'ACCENT4': 'FFFF00', 'ACCENT5': '00FFFF', 'ACCENT6': 'FF00FF',
            'HLINK': '0563C1', 'FOLHLINK': '954F72'
        }
        log_print('[调试] 主题色映射兜底:', color_map)
    log_print('[调试] 主题色映射:', color_map)
    # 新增：自动从PPTX包内加载clrMap
    clrmap = get_clrmap_from_pptx(prs)
    return color_map, clrmap


def parse_slide(pptx_or_prs, slide_idx):
    from pptx import Presentation
    if isinstance(pptx_or_prs, str):
        prs = Presentation(pptx_or_prs)
        prs.pptx_path = pptx_or_prs
    else:
        prs = pptx_or_prs
    theme_color_map, clrmap = get_theme_color_map(prs)
    log_print(f'[clrMap调试] 加载到clrMap: {clrmap}')
    images_dir = os.path.join(os.path.dirname(getattr(prs, 'pptx_path', '')), 'images')
    os.makedirs(images_dir, exist_ok=True)
    result = {
        'images': [],
        'texts': [],
        'shapes': [],
        'tables': [],
        'groups': [],
        'layout': {},
        'master': {},
        'theme_color_map': theme_color_map,
    }
    result['master'] = parse_master(prs)
    shape_idx = 0
    for shape in prs.slides[slide_idx].shapes:
        shape_idx += 1
        log_print(f'[调试] slide.shapes: name={shape.name}, shape_type={shape.shape_type}, str={str(shape.shape_type)}, auto_shape_type={safe_get_auto_shape_type(shape)}, has_text_frame={shape.has_text_frame}')
        if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
            pic = parse_picture(shape, theme_color_map, clrmap)
            log_print(f'[调试] parse_slide 收到图片: name={pic.get("name")}, keys={list(pic.keys())}, image_type={type(pic.get("image", None))}')
            result['images'].append(pic)
        elif shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            grp = parse_group(shape, theme_color_map, clrmap, images_dir, slide_idx+1)
            log_print(f'[调试] parse_slide 收到group: name={grp.get("name")}, children_count={len(grp.get("children", []))}')
            result['groups'].append(grp)
        elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
            result['tables'].append(parse_table(shape, theme_color_map, clrmap))
        elif shape.has_text_frame:
            result['texts'].append(parse_text(shape, theme_color_map, clrmap))
        else:
            result['shapes'].append(parse_shape(shape, theme_color_map, clrmap, images_dir, slide_idx+1, shape_idx))
    result['layout'] = {'elements': parse_layout(prs.slides[slide_idx], theme_color_map, clrmap, images_dir, slide_idx+1)}
    # 新增：递归收集所有图片元素
    def collect_images_from_layout(elements, images):
        if not isinstance(elements, list):
            return
        for elem in elements:
            if isinstance(elem, dict):
                img_obj = elem.get('image', None)
                if (elem.get('shape_type') == 13 or elem.get('shape_type') == 'PICTURE' or (img_obj and str(type(img_obj)).endswith("pptx.parts.image.Image'>"))):
                    log_print(f'[调试] collect_images_from_layout 收到图片: name={elem.get("name")}, keys={list(elem.keys())}, image_type={type(img_obj)}')
                    images.append(elem)
                if 'children' in elem:
                    collect_images_from_layout(elem['children'], images)
    result['images'] = []
    if 'elements' in result['layout']:
        collect_images_from_layout(result['layout']['elements'], result['images'])
    log_print(f'[调试] parse_slide 最终图片数量: {len(result["images"])}, names={[img.get("name") for img in result["images"]]}')
    return result


def parse_master(prs):
    # 这里只返回母版名称和布局名，后续可扩展
    return {
        'master_name': prs.slide_master.name if hasattr(prs.slide_master, 'name') else '',
        'layouts': [l.name for l in prs.slide_master.slide_layouts]
    }


def parse_picture(shape, theme_color_map, clrmap):
    base = {
        'name': shape.name,
        'left': shape.left,
        'top': shape.top,
        'width': shape.width,
        'height': shape.height,
        'rotation': getattr(shape, 'rotation', 0),
        'crop': {
            'left': getattr(shape, 'crop_left', 0),
            'top': getattr(shape, 'crop_top', 0),
            'right': getattr(shape, 'crop_right', 0),
            'bottom': getattr(shape, 'crop_bottom', 0),
        },
        'image': shape.image,
        'z_order': getattr(shape, 'z_order_position', None),
        'shape_type': 'PICTURE',  # 强制标记图片类型
    }
    base.update(get_shape_styles(shape, theme_color_map, clrmap))
    log_print(f'[调试] parse_picture: name={base.get("name")}, image_type={type(base.get("image", None))}, keys={list(base.keys())}')
    return base


# 修改 extract_run_color

def extract_run_color(run, theme_color_map, clrmap=None, shape_element=None, master_default_scheme='tx1'):
    from pptx.dml.color import MSO_COLOR_TYPE
    color_obj = getattr(run.font, 'color', None)
    if color_obj is not None:
        color_type = getattr(color_obj, 'type', None)
        if color_type == MSO_COLOR_TYPE.RGB and getattr(color_obj, 'rgb', None):
            return str(color_obj.rgb)
        elif color_type == MSO_COLOR_TYPE.SCHEME:
            theme_color = getattr(color_obj, 'theme_color', None)
            if theme_color and theme_color_map:
                key = getattr(theme_color, 'name', str(theme_color)).split('(')[0].strip().upper().replace('_', '')
                key = normalize_theme_key(key)
                color = resolve_theme_color(key, theme_color_map, clrmap)
                if color:
                    return color
    elem = getattr(run, '_element', None)
    if elem is not None:
        ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
        solidfill = elem.find('.//a:solidFill', ns)
        if solidfill is not None:
            scheme_clr = solidfill.find('.//a:schemeClr', ns)
            srgb_clr = solidfill.find('.//a:srgbClr', ns)
            prst_clr = solidfill.find('.//a:prstClr', ns)
            if scheme_clr is not None and 'val' in scheme_clr.attrib:
                scheme_val = scheme_clr.attrib['val'].upper()
                scheme_val = normalize_theme_key(scheme_val)
                if theme_color_map:
                    color = resolve_theme_color(scheme_val, theme_color_map, clrmap)
                    if color:
                        return color
            elif srgb_clr is not None and 'val' in srgb_clr.attrib:
                return srgb_clr.attrib['val'].upper()
            elif prst_clr is not None and 'val' in prst_clr.attrib:
                return prst_clr.attrib['val'].lower()
    # 新增：自动色链路，走母版txStyles默认schemeClr
    if theme_color_map and clrmap:
        default_scheme = master_default_scheme.upper().replace('_', '')
        mapped_key = default_scheme
        if clrmap and default_scheme.lower() in clrmap:
            mapped_key = clrmap[default_scheme.lower()].upper().replace('_', '')
        color = theme_color_map.get(mapped_key)
        if color:
            return color
    return None


def parse_text(shape, theme_color_map, clrmap=None):
    runs = []
    shape_element = getattr(shape, 'element', None)
    for p in shape.text_frame.paragraphs:
        for run in p.runs:
            color = extract_run_color(run, theme_color_map, clrmap, shape_element)
            runs.append({
                'text': run.text,
                'font': str(run.font.name) if run.font.name else None,
                'size': int(run.font.size) if run.font.size else None,
                'bold': run.font.bold,
                'italic': run.font.italic,
                'color': color,
            })
    # 新增文本框属性
    tf = shape.text_frame
    margins = {
        'left': int(getattr(tf, 'margin_left', 0)),
        'right': int(getattr(tf, 'margin_right', 0)),
        'top': int(getattr(tf, 'margin_top', 0)),
        'bottom': int(getattr(tf, 'margin_bottom', 0)),
    }
    alignment = None
    vertical_anchor = None
    if tf.paragraphs and len(tf.paragraphs) > 0:
        alignment = str(tf.paragraphs[0].alignment) if tf.paragraphs[0].alignment else None
    if hasattr(tf, 'vertical_anchor'):
        vertical_anchor = str(tf.vertical_anchor)
    base = {
        'name': shape.name,
        'shape_type': safe_get_auto_shape_type(shape) or str(shape.shape_type),
        'left': shape.left,
        'top': shape.top,
        'width': shape.width,
        'height': shape.height,
        'rotation': getattr(shape, 'rotation', 0),
        'z_order': getattr(shape, 'z_order_position', None),
        'runs': runs,
        'autofit': getattr(tf, 'auto_size', None),
        'wrap': getattr(tf, 'word_wrap', None),
        'alignment': alignment,
        'vertical_anchor': vertical_anchor,
        'margins': margins,
    }
    log_print(f'[调试] parse_text结果: {base}')
    base.update(get_shape_styles(shape, theme_color_map, clrmap))
    return base


def parse_table(shape, theme_color_map, clrmap):
    table = shape.table
    data = []
    for row in table.rows:
        data.append([cell.text for cell in row.cells])
    base = {
        'name': shape.name,
        'left': shape.left,
        'top': shape.top,
        'width': shape.width,
        'height': shape.height,
        'rotation': getattr(shape, 'rotation', 0),
        'z_order': getattr(shape, 'z_order_position', None),
        'data': data,
        'row_count': len(table.rows),
        'col_count': len(table.columns),
    }
    base.update(get_shape_styles(shape, theme_color_map, clrmap))
    return base


def parse_group(shape, theme_color_map, clrmap, images_dir=None, slide_num=None):
    children = []
    group_left = shape.left
    group_top = shape.top
    min_left = min((s.left for s in shape.shapes), default=group_left)
    min_top = min((s.top for s in shape.shapes), default=group_top)
    group_left = min_left
    group_top = min_top
    shape_idx = 0
    for s in shape.shapes:
        shape_idx += 1
        log_print(f'[调试] group.shapes: name={s.name}, shape_type={s.shape_type}, str={str(s.shape_type)}, auto_shape_type={safe_get_auto_shape_type(s)}, has_text_frame={s.has_text_frame}')
        child = None
        if s.shape_type == MSO_SHAPE_TYPE.PICTURE:
            child = parse_picture(s, theme_color_map, clrmap)
            log_print(f'[调试] parse_group 收到图片: name={child.get("name")}, keys={list(child.keys())}, image_type={type(child.get("image", None))}')
        elif s.shape_type == MSO_SHAPE_TYPE.GROUP:
            child = parse_group(s, theme_color_map, clrmap, images_dir, slide_num)
        elif s.shape_type == MSO_SHAPE_TYPE.TABLE:
            child = parse_table(s, theme_color_map, clrmap)
        elif s.has_text_frame:
            child = parse_text(s, theme_color_map, clrmap)
        else:
            child = parse_shape(s, theme_color_map, clrmap, images_dir, slide_num, shape_idx)
        if child:
            child['left'] = max(0, s.left - group_left)
            child['top'] = max(0, s.top - group_top)
            children.append(child)
    base = {
        'name': shape.name,
        'shape_type': safe_get_auto_shape_type(shape) or str(shape.shape_type),
        'left': group_left,
        'top': group_top,
        'width': shape.width,
        'height': shape.height,
        'rotation': getattr(shape, 'rotation', 0),
        'z_order': getattr(shape, 'z_order_position', None),
        'children': children,
    }
    log_print(f'[调试] parse_group结果: {base}')
    base.update(get_shape_styles(shape, theme_color_map, clrmap))
    return base


def parse_shape(shape, theme_color_map, clrmap, images_dir=None, slide_num=None, shape_idx=None):
    log_print(f'[调试] parse_shape: name={shape.name}, shape_type={shape.shape_type}, str={str(shape.shape_type)}, auto_shape_type={safe_get_auto_shape_type(shape)}, has_text_frame={shape.has_text_frame}')
    auto_type = str(safe_get_auto_shape_type(shape)).upper()
    shape_type_str = str(shape.shape_type).upper()
    is_custom_geom = False
    try:
        elem = getattr(shape, 'element', None)
        if elem is not None:
            xml_str = ET.tostring(elem, encoding='unicode')
            if '<a:custGeom' in xml_str:
                is_custom_geom = True
    except Exception as e:
        log_print('[调试] custom geometry XML判断异常:', e)
    if is_custom_geom or auto_type == 'CUSTOM_GEOMETRY' or 'FREEFORM' in shape_type_str or 'CUSTOM' in shape_type_str or '自定义' in shape.name or '多边形' in shape.name:
        log_print(f'[调试] 进入多边形导出分支: {shape.name}, {shape.shape_type}')
        log_print(f'[调试] shape.image 是否存在: {hasattr(shape, "image")}, type: {type(getattr(shape, "image", None))}')
        if images_dir and slide_num is not None and shape_idx is not None:
            img_path = os.path.join(images_dir, f"slide{slide_num}_shape{shape_idx}.png")
            try:
                shape.image.save(img_path)
                log_print(f'[调试] 多边形图片已保存: {img_path}')
            except Exception as e:
                log_print(f'[调试] 多边形导出图片异常: {e}')
                img_path = None
        else:
            img_path = None
        base = {
            'name': shape.name,
            'shape_type': str(shape.shape_type),
            'type': 'image',
            'image_path': img_path,
            'left': shape.left,
            'top': shape.top,
            'width': shape.width,
            'height': shape.height,
            'rotation': getattr(shape, 'rotation', 0),
            'z_order': getattr(shape, 'z_order_position', None),
        }
        log_print(f'[调试] parse_shape(图片)结果: {base}')
        base.update(get_shape_styles(shape, theme_color_map, clrmap))
        return base
    else:
        log_print(f'[调试] 未进入多边形导出分支: {shape.name}, {shape.shape_type}, auto_type={auto_type}, is_custom_geom={is_custom_geom}')
    base = {
        'name': shape.name,
        'shape_type': safe_get_auto_shape_type(shape) or str(shape.shape_type),
        'left': shape.left,
        'top': shape.top,
        'width': shape.width,
        'height': shape.height,
        'rotation': getattr(shape, 'rotation', 0),
        'z_order': getattr(shape, 'z_order_position', None),
    }
    log_print(f'[调试] parse_shape(普通)结果: {base}')
    base.update(get_shape_styles(shape, theme_color_map, clrmap))
    return base


def parse_layout(slide, theme_color_map, clrmap, images_dir=None, slide_num=None):
    layout = []
    shape_idx = 0
    for shape in slide.shapes:
        shape_idx += 1
        if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            group_info = parse_group(shape, theme_color_map, clrmap, images_dir, slide_num)
            layout.append(group_info)
        elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
            layout.append(parse_picture(shape, theme_color_map, clrmap))
        elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
            layout.append(parse_table(shape, theme_color_map, clrmap))
        elif shape.has_text_frame:
            layout.append(parse_text(shape, theme_color_map, clrmap))
        else:
            layout.append(parse_shape(shape, theme_color_map, clrmap, images_dir, slide_num, shape_idx))
    return layout

# 在主解析流程最后，输出所有未命中的主题色key
if __name__ == '__main__':
    # ... existing code ...
    if missed_theme_keys:
        print('\n[主题色查表未命中统计]')
        for key, shape in missed_theme_keys:
            print(f'未命中主题色: {key}，形状: {shape}')

def extract_scheme_color_with_lum(element, theme_color_map, expect_key=None, clrmap=None):
    ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
    scheme_clrs = element.findall('.//a:schemeClr', ns)
    def get_final_rgb(key, lum_mod, lum_off):
        norm_key = key.upper().replace('_', '')
        if clrmap:
            log_print(f'[clrMap调试] clrMap内容: {clrmap}')
        if clrmap and norm_key.lower() in clrmap:
            mapped_key = clrmap[norm_key.lower()]
            mapped_key = mapped_key.upper().replace('_', '')
            log_print(f'[clrMap调试] key={key} 命中clrMap: {norm_key.lower()}->{mapped_key}')
        else:
            mapped_key = norm_key
            log_print(f'[clrMap调试] key={key} 未命中clrMap, 用原key: {mapped_key}')
        base_rgb = theme_color_map.get(mapped_key)
        log_print(f'[clrMap调试] theme_color_map查表: mapped_key={mapped_key}, result={base_rgb}')
        if not base_rgb:
            return None
        rgb_tuple = hex_to_rgb(base_rgb)
        rgb = apply_lum_mod_off(rgb_tuple, lum_mod, lum_off)
        return rgb_to_hex(rgb)
    # 优先查找完全匹配的key
    for scheme_clr in scheme_clrs:
        key = scheme_clr.attrib.get('val', '').upper().replace('_', '')
        if expect_key and normalize_theme_key(key) != normalize_theme_key(expect_key):
            continue
        lum_mod = None
        lum_off = None
        for child in scheme_clr:
            tag = child.tag.split('}')[-1]
            if tag == 'lumMod':
                lum_mod = int(child.attrib.get('val', '100000'))
            elif tag == 'lumOff':
                lum_off = int(child.attrib.get('val', '0'))
        result = get_final_rgb(key, lum_mod, lum_off)
        log_print(f'[调试] extract_scheme_color_with_lum: key={key}, lum_mod={lum_mod}, lum_off={lum_off}, result={result}')
        if result:
            return result
    # 若未找到完全匹配的key，尝试第一个可用schemeClr
    for scheme_clr in scheme_clrs:
        key = scheme_clr.attrib.get('val', '').upper().replace('_', '')
        lum_mod = None
        lum_off = None
        for child in scheme_clr:
            tag = child.tag.split('}')[-1]
            if tag == 'lumMod':
                lum_mod = int(child.attrib.get('val', '100000'))
            elif tag == 'lumOff':
                lum_off = int(child.attrib.get('val', '0'))
        result = get_final_rgb(key, lum_mod, lum_off)
        if result:
            return result
    return None 